/**
 * Copyright (c) 2006, www.jempbox.org
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of pdfbox; nor the names of its
 *    contributors may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * http://www.jempbox.org
 *
 */
package org.jempbox.xmp;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.jempbox.impl.DateConverter;
import org.jempbox.impl.XMLUtil;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This class represents a metadata schema that can be stored in an XMP
 * document. It handles all generic properties that are available. See
 * subclasses for access to specific properties.
 * 
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * @version $Revision: 1.8 $
 */
public class XMPSchema {
	/**
	 * The standard xmlns namespace.
	 */
	public static final String NS_NAMESPACE = "http://www.w3.org/2000/xmlns/";

	/**
	 * The XML schema prefix.
	 */
	protected String prefix;

	/**
	 * The DOM representation of this object.
	 */
	protected Element schema = null;

	/**
	 * Create a new blank schema that can be populated.
	 * 
	 * @param parent
	 *            The parent XMP document that this schema will be part of.
	 * @param namespaceName
	 *            The name of the namespace, ie pdf,dc,...
	 * @param namespaceURI
	 *            The URI of the namespace, ie "http://ns.adobe.com/pdf/1.3/"
	 */
	public XMPSchema(XMPMetadata parent, String namespaceName,
			String namespaceURI) {
		schema = parent.xmpDocument.createElementNS(
				"http://www.w3.org/1999/02/22-rdf-syntax-ns#",
				"rdf:Description");
		prefix = namespaceName;
		schema.setAttributeNS(NS_NAMESPACE, "xmlns:" + namespaceName,
				namespaceURI);
	}

	/**
	 * Create schema from an existing XML element.
	 * 
	 * @param element
	 *            The existing XML element.
	 * @param aPrefix
	 *            The XML prefix.
	 */
	public XMPSchema(Element element, String aPrefix) {
		schema = element;
		if (aPrefix != null) {
			prefix = aPrefix;
		} else {
			prefix = "";
		}
	}

	/**
	 * Get the XML element that is represented by this schema.
	 * 
	 * @return The root XML element of this schema.
	 */
	public Element getElement() {
		return schema;
	}

	/**
	 * Get the RDF about attribute.
	 * 
	 * @return The RDF 'about' attribute.
	 */
	public String getAbout() {
		return getTextProperty("rdf:about");
	}

	/**
	 * Set the RDF 'about' attribute. Passing in null will clear this attribute.
	 * 
	 * @param about
	 *            The new RFD about value.
	 */
	public void setAbout(String about) {
		if (about == null) {
			schema.removeAttribute("rdf:about");
		} else {
			schema.setAttribute("rdf:about", about);
		}
	}

	/**
	 * Set a simple text property on the schema.
	 * 
	 * @param propertyName
	 *            The name of the property, it must contain the namespace
	 *            prefix, ie "pdf:Keywords"
	 * @param propertyValue
	 *            The value for the property, can be any string. Passing null
	 *            will remove the property.
	 */
	public void setTextProperty(String propertyName, String propertyValue) {
		if (propertyValue == null) {
			//schema.removeAttribute(propertyName);
			NodeList keywordList = schema.getElementsByTagName(propertyName);
			for (int i = 0; i < keywordList.getLength(); i++) {
				schema.removeChild(keywordList.item(i));
			}

		} else {
			//System.out.println(propertyName + "-" + propertyValue);
			if (schema.hasChildNodes()) {
				//System.out.println("1");
				NodeList nodeList = schema.getElementsByTagName(propertyName);
				if (nodeList.getLength() > 0) {
					//System.out.println("2");
					Element node = (Element) nodeList.item(0);
					//System.out.println(node.getTextContent();
					node.setTextContent(propertyValue);
				} else {
					//System.out.println("3");
					Element textNode = schema.getOwnerDocument().createElement(
							propertyName);
					XMLUtil.setStringValue(textNode, propertyValue);
					schema.appendChild(textNode);
				}
			} else {
				//System.out.println("10");
				Element textNode = schema.getOwnerDocument().createElement(
						propertyName);
				XMLUtil.setStringValue(textNode, propertyValue);
				schema.appendChild(textNode);
			}

		}
	}

	/**
	 * Get the value of a simple text property.
	 * 
	 * @param propertyName
	 *            The name of the property to get, it must include the namespace
	 *            prefix. ie "pdf:Keywords".
	 * 
	 * @return The value of the text property or the null if there is no value.
	 */
	public String getTextProperty(String propertyName) {
		// propertyValue == null does not work, since getAttribute returns the
		// empty string if the attribute is not found

		if (schema.hasAttribute(propertyName)) {
			return schema.getAttribute(propertyName);
		} else {
			NodeList nodes = schema.getElementsByTagName(propertyName);
			if (nodes.getLength() > 0) {
				Element node = (Element) nodes.item(0);
				return XMLUtil.getStringValue(node);
			}
			return null;
		}
	}

	/**
	 * Get the value of the property as a date.
	 * 
	 * @param propertyName
	 *            The fully qualified property name for the date.
	 * 
	 * @return The value of the property as a date.
	 * 
	 * @throws IOException
	 *             If there is an error converting the value to a date.
	 */
	public Calendar getDateProperty(String propertyName) throws IOException {
		return DateConverter.toCalendar(getTextProperty(propertyName));
	}

	/**
	 * Set the value of the property as a date.
	 * 
	 * @param propertyName
	 *            The fully qualified property name for the date.
	 * @param date
	 *            The date to set, or null to clear.
	 */
	public void setDateProperty(String propertyName, Calendar date) {
		setTextProperty(propertyName, DateConverter.toISO8601(date));
	}

	/**
	 * Get the value of the property as a boolean.
	 * 
	 * @param propertyName
	 *            The fully qualified property name for the boolean.
	 * 
	 * @return The value of the property as a boolean.
	 */
	public Boolean getBooleanProperty(String propertyName) {
		Boolean value = null;
		String stringValue = getTextProperty(propertyName);
		if (stringValue != null) {
			value = stringValue.equals("True") ? Boolean.TRUE : Boolean.FALSE;
		}
		return value;
	}

	/**
	 * Set the value of the property as a boolean.
	 * 
	 * @param propertyName
	 *            The fully qualified property name for the boolean.
	 * @param bool
	 *            The boolean to set, or null to clear.
	 */
	public void setBooleanProperty(String propertyName, Boolean bool) {
		String value = null;
		if (bool != null) {
			value = bool.booleanValue() ? "True" : "False";
		}
		setTextProperty(propertyName, value);
	}

	/**
	 * Get the value of the property as an integer.
	 * 
	 * @param propertyName
	 *            The fully qualified property name for the integer.
	 * 
	 * @return The value of the property as an integer.
	 */
	public Integer getIntegerProperty(String propertyName) {
		Integer retval = null;
		String intProperty = getTextProperty(propertyName);
		if (intProperty.equals("")) {
			retval = new Integer(intProperty);
		}
		return retval;
	}

	/**
	 * Set the value of the property as an integer.
	 * 
	 * @param propertyName
	 *            The fully qualified property name for the integer.
	 * @param intValue
	 *            The int to set, or null to clear.
	 */
	public void setIntegerProperty(String propertyName, Integer intValue) {
		String textValue = null;
		if (intValue != null) {
			textValue = intValue.toString();
		}
		setTextProperty(propertyName, textValue);
	}

	/**
	 * Remove all matching entries with the given value from the bag.
	 * 
	 * @param bagName
	 *            The name of the bag, it must include the namespace prefix. ie
	 *            "pdf:Keywords".
	 * @param bagValue
	 *            The value to remove from the bagList.
	 */
	public void removeBagValue(String bagName, String bagValue) {
		Element bagElement = null;
		NodeList nodes = schema.getElementsByTagName(bagName);
		if (nodes.getLength() > 0) {
			Element contElement = (Element) nodes.item(0);
			NodeList bagList = contElement.getElementsByTagName("rdf:Bag");
			if (bagList.getLength() > 0) {
				bagElement = (Element) bagList.item(0);
				NodeList items = bagElement.getElementsByTagName("rdf:li");
				for (int i = items.getLength() - 1; i >= 0; i--) {
					Element li = (Element) items.item(i);
					String value = XMLUtil.getStringValue(li);
					if (value.equals(bagValue)) {
						bagElement.removeChild(li);
					}
				}
			}
		}
	}

	/**
	 * Add an entry to a bag property.
	 * 
	 * @param bagName
	 *            The name of the bag, it must include the namespace prefix. ie
	 *            "pdf:Keywords".
	 * @param bagValue
	 *            The value to add to the bagList.
	 */
	public void addBagValue(String bagName, String bagValue) {
		Element bagElement = null;
		NodeList nodes = schema.getElementsByTagName(bagName);
		if (nodes.getLength() > 0) {
			Element contElement = (Element) nodes.item(0);
			NodeList bagList = contElement.getElementsByTagName("rdf:Bag");
			if (bagList.getLength() > 0) {
				bagElement = (Element) bagList.item(0);
			}
		} else {
			Element contElement = schema.getOwnerDocument().createElement(
					bagName);
			schema.appendChild(contElement);
			bagElement = schema.getOwnerDocument().createElement("rdf:Bag");
			contElement.appendChild(bagElement);
		}
		Element liElement = schema.getOwnerDocument().createElement("rdf:li");
		XMLUtil.setStringValue(liElement, bagValue);
		bagElement.appendChild(liElement);
	}

	/**
	 * Get all the values of the bag property. This will return a list of
	 * java.lang.String objects, this is a read-only list.
	 * 
	 * @param bagName
	 *            The name of the bag property to get, it must include the
	 *            namespace prefix. ie "pdf:Keywords"
	 * 
	 * @return All of the values of the bag property in a list.
	 */
	public List getBagList(String bagName) {
		List retval = null;
		NodeList nodes = schema.getElementsByTagName(bagName);
		if (nodes.getLength() > 0) {
			Element contributor = (Element) nodes.item(0);
			NodeList bagList = contributor.getElementsByTagName("rdf:Bag");
			if (bagList.getLength() > 0) {
				Element bag = (Element) bagList.item(0);
				retval = new ArrayList();
				NodeList items = bag.getElementsByTagName("rdf:li");
				for (int i = 0; i < items.getLength(); i++) {
					Element li = (Element) items.item(i);
					retval.add(XMLUtil.getStringValue(li));
				}
				retval = Collections.unmodifiableList(retval);
			}
		}

		return retval;
	}

	/**
	 * Remove all matching values from a sequence property.
	 * 
	 * @param seqName
	 *            The name of the sequence property. It must include the
	 *            namespace prefix. ie "pdf:Keywords".
	 * @param seqValue
	 *            The value to remove from the list.
	 */
	public void removeSequenceValue(String seqName, String seqValue) {
		Element bagElement = null;
		NodeList nodes = schema.getElementsByTagName(seqName);
		if (nodes.getLength() > 0) {
			Element contElement = (Element) nodes.item(0);
			NodeList bagList = contElement.getElementsByTagName("rdf:Seq");
			if (bagList.getLength() > 0) {
				bagElement = (Element) bagList.item(0);
				NodeList items = bagElement.getElementsByTagName("rdf:li");
				for (int i = items.getLength() - 1; i >= 0; i--) {
					Element li = (Element) items.item(i);
					String value = XMLUtil.getStringValue(li);
					if (value.equals(seqValue)) {
						bagElement.removeChild(li);
					}
				}
			}
		}
	}

	/**
	 * Remove a value from a sequence property. This will remove all entries
	 * from the list.
	 * 
	 * @param seqName
	 *            The name of the sequence property. It must include the
	 *            namespace prefix. ie "pdf:Keywords".
	 * @param seqValue
	 *            The value to remove from the list.
	 */
	public void removeSequenceValue(String seqName, Elementable seqValue) {
		Element bagElement = null;
		NodeList nodes = schema.getElementsByTagName(seqName);
		if (nodes.getLength() > 0) {
			Element contElement = (Element) nodes.item(0);
			NodeList bagList = contElement.getElementsByTagName("rdf:Seq");
			if (bagList.getLength() > 0) {
				bagElement = (Element) bagList.item(0);
				NodeList items = bagElement.getElementsByTagName("rdf:li");
				for (int i = 0; i < items.getLength(); i++) {
					Element li = (Element) items.item(i);
					if (li == seqValue.getElement()) {
						bagElement.removeChild(li);
					}
				}
			}
		}
	}

	/**
	 * Add a new value to a sequence property.
	 * 
	 * @param seqName
	 *            The name of the sequence property, it must include the
	 *            namespace prefix. ie "pdf:Keywords"
	 * @param seqValue
	 *            The value to add to the sequence.
	 */
	public void addSequenceValue(String seqName, String seqValue) {
		Element bagElement = null;
		NodeList nodes = schema.getElementsByTagName(seqName);
		if (nodes.getLength() > 0) {
			Element contElement = (Element) nodes.item(0);
			NodeList bagList = contElement.getElementsByTagName("rdf:Seq");
			if (bagList.getLength() > 0) {
				bagElement = (Element) bagList.item(0);
			} else {
				// xml is crap discard it
				schema.removeChild(nodes.item(0));
			}
		}
		if (bagElement == null) {
			Element contElement = schema.getOwnerDocument().createElement(
					seqName);
			schema.appendChild(contElement);
			bagElement = schema.getOwnerDocument().createElement("rdf:Seq");
			contElement.appendChild(bagElement);
		}
		Element liElement = schema.getOwnerDocument().createElement("rdf:li");
		liElement.appendChild(schema.getOwnerDocument()
				.createTextNode(seqValue));
		bagElement.appendChild(liElement);
	}

	/**
	 * Add a new value to a sequence property.
	 * 
	 * @param seqName
	 *            The name of the sequence property, it must include the
	 *            namespace prefix. ie "pdf:Keywords"
	 * @param seqValue
	 *            The value to add to the sequence.
	 */
	public void addSequenceValue(String seqName, Elementable seqValue) {
		Element bagElement = null;
		NodeList nodes = schema.getElementsByTagName(seqName);
		if (nodes.getLength() > 0) {
			Element contElement = (Element) nodes.item(0);
			NodeList bagList = contElement.getElementsByTagName("rdf:Seq");
			if (bagList.getLength() > 0) {
				bagElement = (Element) bagList.item(0);
			}
		} else {
			Element contElement = schema.getOwnerDocument().createElement(
					seqName);
			schema.appendChild(contElement);
			bagElement = schema.getOwnerDocument().createElement("rdf:Seq");
			contElement.appendChild(bagElement);
		}
		bagElement.appendChild(seqValue.getElement());
	}

	/**
	 * Get all the values in a sequence property.
	 * 
	 * @param seqName
	 *            The name of the sequence property, it must include the
	 *            namespace prefix. ie "pdf:Keywords".
	 * 
	 * @return A read-only list of java.lang.String objects or null if the
	 *         property does not exist.
	 */
	public List getSequenceList(String seqName) {
		List retval = null;
		NodeList nodes = schema.getElementsByTagName(seqName);
		if (nodes.getLength() > 0) {
			Element contributor = (Element) nodes.item(0);
			NodeList bagList = contributor.getElementsByTagName("rdf:Seq");
			if (bagList.getLength() > 0) {
				Element bag = (Element) bagList.item(0);
				retval = new ArrayList();
				NodeList items = bag.getElementsByTagName("rdf:li");
				for (int i = 0; i < items.getLength(); i++) {
					Element li = (Element) items.item(i);
					retval.add(XMLUtil.getStringValue(li));
				}
				retval = Collections.unmodifiableList(retval);
			}
		}

		return retval;
	}

	/**
	 * Get a list of ResourceEvent objects.
	 * 
	 * @param seqName
	 *            The name of the sequence to retrieve.
	 * 
	 * @return A list of ResourceEvent objects or null if they do not exist.
	 */
	public List getEventSequenceList(String seqName) {
		List retval = null;
		NodeList nodes = schema.getElementsByTagName(seqName);
		if (nodes.getLength() > 0) {
			Element contributor = (Element) nodes.item(0);
			NodeList bagList = contributor.getElementsByTagName("rdf:Seq");
			if (bagList.getLength() > 0) {
				Element bag = (Element) bagList.item(0);
				retval = new ArrayList();
				NodeList items = bag.getElementsByTagName("rdf:li");
				for (int i = 0; i < items.getLength(); i++) {
					Element li = (Element) items.item(i);
					retval.add(new ResourceEvent(li));
				}
				retval = Collections.unmodifiableList(retval);
			}
		}

		return retval;
	}

	/**
	 * Remove a date sequence value from the list.
	 * 
	 * @param seqName
	 *            The name of the sequence property, it must include the
	 *            namespace prefix. ie "pdf:Keywords"
	 * @param date
	 *            The date to remove from the sequence property.
	 */
	public void removeSequenceDateValue(String seqName, Calendar date) {
		String dateAsString = DateConverter.toISO8601(date);
		removeSequenceValue(seqName, dateAsString);
	}

	/**
	 * Add a date sequence value to the list.
	 * 
	 * @param seqName
	 *            The name of the sequence property, it must include the
	 *            namespace prefix. ie "pdf:Keywords"
	 * @param date
	 *            The date to add to the sequence property.
	 */
	public void addSequenceDateValue(String seqName, Calendar date) {
		String dateAsString = DateConverter.toISO8601(date);
		addSequenceValue(seqName, dateAsString);
	}

	/**
	 * Get all the date values in a sequence property.
	 * 
	 * @param seqName
	 *            The name of the sequence property, it must include the
	 *            namespace prefix. ie "pdf:Keywords".
	 * 
	 * @return A read-only list of java.util.Calendar objects or null if the
	 *         property does not exist.
	 * 
	 * @throws IOException
	 *             If there is an error converting the value to a date.
	 */
	public List getSequenceDateList(String seqName) throws IOException {
		List strings = getSequenceList(seqName);
		List retval = null;
		if (strings != null) {
			retval = new ArrayList();
			for (int i = 0; i < strings.size(); i++) {
				retval.add(DateConverter.toCalendar((String) strings.get(i)));
			}
		}
		return retval;
	}

	/**
	 * Set the value of a multi-lingual property.
	 * 
	 * @param propertyName
	 *            The name of the property, it must include the namespace
	 *            prefix. ie "pdf:Keywords"
	 * @param language
	 *            The language code of the value. If null then "x-default" is
	 *            assumed.
	 * @param value
	 *            The value of the property in the specified language.
	 */
	public void setLanguageProperty(String propertyName, String language,
			String value) {
		NodeList nodes = schema.getElementsByTagName(propertyName);
		Element property = null;
		if (nodes.getLength() == 0) {
			if (value == null) {
				// value is null, it doesn't already exist so there
				// is nothing to do.
				return;
			}
			property = schema.getOwnerDocument().createElement(propertyName);
			schema.appendChild(property);
		} else {
			property = (Element) nodes.item(0);
		}
		Element alt = null;
		NodeList altList = property.getElementsByTagName("rdf:Alt");
		if (altList.getLength() == 0) {
			if (value == null) {
				// value is null, it doesn't already exist so there
				// is nothing to do.
				return;
			}
			alt = schema.getOwnerDocument().createElement("rdf:Alt");
			property.appendChild(alt);
		} else {
			alt = (Element) altList.item(0);
		}
		NodeList items = alt.getElementsByTagName("rdf:li");
		if (language == null) {
			language = "x-default";
		}
		boolean foundValue = false;
		for (int i = 0; i < items.getLength(); i++) {
			Element li = (Element) items.item(i);
			if (value == null) {
				alt.removeChild(li);
			} else if (language.equals(li.getAttribute("xml:lang"))) {
				foundValue = true;
				XMLUtil.setStringValue(li, value);
			}
		}
		if (value != null && !foundValue) {
			Element li = schema.getOwnerDocument().createElement("rdf:li");
			li.setAttribute("xml:lang", language);
			XMLUtil.setStringValue(li, value);
			if (language.equals("x-default")) {
				// default should be first element, see XMP spec
				alt.insertBefore(li, alt.getFirstChild());
			} else {
				alt.appendChild(li);
			}

		}
	}

	/**
	 * Get the value of a multi-lingual property.
	 * 
	 * @param propertyName
	 *            The name of the property, it must include the namespace
	 *            prefix. ie "pdf:Keywords"
	 * @param language
	 *            The language code of the value. If null then "x-default" is
	 *            assumed.
	 * 
	 * @return The value of the language property.
	 */
	public String getLanguageProperty(String propertyName, String language) {
		String retval = null;
		if (language == null) {
			language = "x-default";
		}

		NodeList nodes = schema.getElementsByTagName(propertyName);
		if (nodes.getLength() > 0) {
			Element property = (Element) nodes.item(0);
			NodeList altList = property.getElementsByTagName("rdf:Alt");
			if (altList.getLength() > 0) {
				Element alt = (Element) altList.item(0);
				NodeList items = alt.getElementsByTagName("rdf:li");
				for (int i = 0; i < items.getLength() && retval == null; i++) {
					Element li = (Element) items.item(i);
					String elementLanguage = li.getAttribute("xml:lang");
					if (language.equals(elementLanguage)) {
						retval = XMLUtil.getStringValue(li);
					}
				}
			}
		}
		return retval;
	}

	/**
	 * Set the value of a multi-lingual property.
	 * 
	 * @param propertyName
	 *            The name of the property, it must include the namespace
	 *            prefix. ie "pdf:Keywords"
	 * @param language
	 *            The language code of the value. If null then "x-default" is
	 *            assumed.
	 * @param value
	 *            The value of the property in the specified language.
	 */
	public void setThumbnailProperty(String propertyName, String language,
			Thumbnail value) {
		NodeList nodes = schema.getElementsByTagName(propertyName);
		Element property = null;
		if (nodes.getLength() == 0) {
			if (value == null) {
				// value is null, it doesn't already exist so there
				// is nothing to do.
				return;
			}
			property = schema.getOwnerDocument().createElement(propertyName);
			schema.appendChild(property);
		} else {
			property = (Element) nodes.item(0);
		}
		Element alt = null;
		NodeList altList = property.getElementsByTagName("rdf:Alt");
		if (altList.getLength() == 0) {
			if (value == null) {
				// value is null, it doesn't already exist so there
				// is nothing to do.
				return;
			}
			alt = schema.getOwnerDocument().createElement("rdf:Alt");
			property.appendChild(alt);
		} else {
			alt = (Element) altList.item(0);
		}
		NodeList items = alt.getElementsByTagName("rdf:li");
		if (language == null) {
			language = "x-default";
		}
		boolean foundValue = false;
		for (int i = 0; i < items.getLength(); i++) {
			Element li = (Element) items.item(i);
			if (value == null) {
				alt.removeChild(li);
			} else if (language.equals(li.getAttribute("xml:lang"))) {
				foundValue = true;
				alt.replaceChild(li, value.getElement());
			}
		}
		if (value != null && !foundValue) {
			Element li = value.getElement();
			li.setAttribute("xml:lang", language);
			if (language.equals("x-default")) {
				// default should be first element, see XMP spec
				alt.insertBefore(li, alt.getFirstChild());
			} else {
				alt.appendChild(li);
			}

		}
	}

	/**
	 * Get the value of a multi-lingual property.
	 * 
	 * @param propertyName
	 *            The name of the property, it must include the namespace
	 *            prefix. ie "pdf:Keywords"
	 * @param language
	 *            The language code of the value. If null then "x-default" is
	 *            assumed.
	 * 
	 * @return The value of the language property.
	 */
	public Thumbnail getThumbnailProperty(String propertyName, String language) {
		Thumbnail retval = null;
		if (language == null) {
			language = "x-default";
		}

		NodeList nodes = schema.getElementsByTagName(propertyName);
		if (nodes.getLength() > 0) {
			Element property = (Element) nodes.item(0);
			NodeList altList = property.getElementsByTagName("rdf:Alt");
			if (altList.getLength() > 0) {
				Element alt = (Element) altList.item(0);
				NodeList items = alt.getElementsByTagName("rdf:li");
				for (int i = 0; i < items.getLength() && retval == null; i++) {
					Element li = (Element) items.item(i);
					String elementLanguage = li.getAttribute("xml:lang");
					if (language.equals(elementLanguage)) {
						retval = new Thumbnail(li);
					}
				}
			}
		}
		return retval;
	}

	/**
	 * Get a list of all languages that are currently defined for a specific
	 * property.
	 * 
	 * @param propertyName
	 *            The name of the property, it must include the namespace
	 *            prefix. ie "pdf:Keywords"
	 * 
	 * @return A list of all languages, this will return an non-null empty list
	 *         if none have been defined.
	 */
	public List getLanguagePropertyLanguages(String propertyName) {
		List retval = new ArrayList();

		NodeList nodes = schema.getElementsByTagName(propertyName);
		if (nodes.getLength() > 0) {
			Element property = (Element) nodes.item(0);
			NodeList altList = property.getElementsByTagName("rdf:Alt");
			if (altList.getLength() > 0) {
				Element alt = (Element) altList.item(0);
				NodeList items = alt.getElementsByTagName("rdf:li");
				for (int i = 0; i < items.getLength(); i++) {
					Element li = (Element) items.item(i);
					String elementLanguage = li.getAttribute("xml:lang");
					if (elementLanguage == null) {
						retval.add("x-default");
					} else {
						retval.add(elementLanguage);
					}
				}
			}
		}
		return retval;
	}

	/**
	 * A basic schema merge, it merges bags and sequences and replace everything
	 * else.
	 * 
	 * @param xmpSchema
	 *            The schema to merge.
	 * @throws IOException
	 *             If there is an error during the merge.
	 */
	public void merge(XMPSchema xmpSchema) throws IOException {
		if (!xmpSchema.getClass().equals(this.getClass())) {
			throw new IOException("Can only merge schemas of the same type.");
		}

		NamedNodeMap attributes = xmpSchema.getElement().getAttributes();
		for (int i = 0; i < attributes.getLength(); i++) {
			Node a = attributes.item(i);
			String name = a.getNodeName();
			if (name.startsWith(prefix)) {
				String newValue = xmpSchema.getTextProperty(name);
				setTextProperty(name, newValue);
			}
		}
		NodeList nodes = xmpSchema.getElement().getChildNodes();

		for (int i = 0; i < nodes.getLength(); i++) {
			Node a = nodes.item(i);
			String name = a.getNodeName();
			if (name.startsWith(prefix)) {
				if (a instanceof Element) {
					Element e = (Element) a;
					if (nodes.getLength() > 0) {
						NodeList seqList = e.getElementsByTagName("rdf:Seq");
						if (seqList.getLength() > 0) {
							List newList = xmpSchema.getSequenceList(name);
							List oldList = getSequenceList(name);

							Iterator it = newList.iterator();

							while (it.hasNext()) {
								Object object = it.next();
								if (oldList == null
										|| !oldList.contains(object)) {
									addSequenceValue(name, (String) object);
								}
							}
							continue;
						}
						NodeList bagList = e.getElementsByTagName("rdf:Bag");
						if (bagList.getLength() > 0) {
							List newList = xmpSchema.getBagList(name);
							List oldList = getBagList(name);

							Iterator it = newList.iterator();

							while (it.hasNext()) {
								Object object = it.next();
								if (oldList == null
										|| !oldList.contains(object)) {
									addBagValue(name, (String) object);
								}
							}
							continue;
						}
					}
				}
				String newValue = xmpSchema.getTextProperty(name);
				setTextProperty(name, newValue);
			}
		}
	}
	public void removeTag(String tagName){
		NodeList nodes = schema.getElementsByTagName(tagName);
		for (int i = nodes.getLength()-1;i>=0;i--){
			schema.removeChild(nodes.item(i));
		}
	}
}